///<copyright>
///-----------------------------------------------------------------------------
/// LoadingScreen.cs
///
/// Microsoft XNA Community Game Platform
/// Copyright (C) Microsoft Corporation. All rights reserved.
///-----------------------------------------------------------------------------
///</copyright>

using System;
using System.Threading;
using System.Diagnostics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Net;
using System.ComponentModel;
using Microsoft.Xna.Framework.Content;

namespace Ferpect.ComponentModel
{
    /// <summary>
    /// The loading screen coordinates transitions between the menu system and the
    /// game itself. Normally one screen will transition off at the same time as
    /// the next screen is transitioning on, but for larger transitions that can
    /// take a longer time to load their data, we want the menu system to be entirely
    /// gone before we start loading the game. This is done as follows:
    /// 
    /// - Tell all the existing screens to transition off.
    /// - Activate a loading screen, which will transition on at the same time.
    /// - The loading screen watches the state of the previous screens.
    /// - When it sees they have finished transitioning off, it activates the real
    ///   next screen, which may take a long time to load its data. The loading
    ///   screen will be the only thing displayed while this load is taking place.
    /// </summary>
    [DefaultProperty("NextScreen")]
    public class LoadingScreen : TransitionScreen
    {
        private Thread backgroundThread;
        private EventWaitHandle backgroundThreadExit;
        private GameTime loadStartTime;
        private bool updating;

        /// <summary>
        /// The constructor is private: loading screens should
        /// be activated via the static Load method instead.
        /// </summary>
        public LoadingScreen()
        {
            this.TransitionOnSeconds = 0.5;

            this.backgroundThread = new Thread(BackgroundWorkerThread);
            this.backgroundThreadExit = new ManualResetEvent(false);
        }

        /// <summary>
        /// Override this method to perform lengthy content loading. This
        /// method will be invoked when the loading screen is activated.
        /// Use the LoadContent method to load content to be using in the
        /// loading screen's animation while this method is executing.
        /// </summary>
        protected virtual void OnDeferredLoadContent(ContentManager contentManager)
        {
        }

        /// <summary>
        /// This method will be used to get the content manager that will be
        /// passed to OnDeferredLoadContent. The base implementation simply
        /// returns the value of the Content property. Override this method
        /// to load your deferred content into a specific content manager that
        /// you can unload without affecting global/shared content.
        /// </summary>
        /// <returns></returns>
        protected virtual ContentManager GetContentManager()
        {
            return this.Content;
        }

        [DefaultValue(null)]
        [Description("Specifies the name of the screen to activate after loading is complete.")]
        public string NextScreen { get; set; }

        private void GotoNextScreen()
        {
            string nextScreen = this.GetNextScreenName();
            this.ScreenManager.Goto(nextScreen, 0);
        }

        /// <summary>
        /// This method is used to determine the screen to activate after
        /// loading is complete. The base implementation returns the value
        /// of the NextScreen property. Override this method if you want to
        /// provide more advanced logic, such as a single loading screen
        /// for several different game screens.
        /// </summary>
        /// <returns>The name of the screen to activate after the background
        /// loading is complete.</returns>
        protected virtual string GetNextScreenName()
        {
            return this.NextScreen;
        }

        #region Update and Draw

        /// <summary>
        /// Updates the loading screen. When loading is finished, the screen
        /// will automatically activate the next screen.
        /// </summary>
        public override void Update(GameTime gameTime)
        {
            base.Update(gameTime);
            if (this.updating)
            {
                return;
            }

            this.updating = true;
            try
            {
                // If all the previous screens have finished transitioning
                // off, it is time to actually perform the load.
                if ((this.ScreenState == ScreenState.On) && this.HasFocus)
                {
                    // Start up the background thread, which will update the network
                    // session and draw the animation while we are loading.
                    this.loadStartTime = gameTime;
                    this.backgroundThread.Start();

                    try
                    {
                        // Load the content here, on the main thread.
                        this.OnDeferredLoadContent(this.GetContentManager());
                    }
                    finally
                    {
                        // Signal the background thread to exit, then wait for it to do so.
                        this.backgroundThreadExit.Set();
                    }
                    this.backgroundThread.Join();

                    // Create a new thread for the next time (threads can't be restarted).
                    this.backgroundThread = new Thread(BackgroundWorkerThread);
                    this.backgroundThreadExit.Reset();

                    // Once the load has finished, we use ResetElapsedTime to tell
                    // the game timing mechanism that we have just finished a very
                    // long frame, and that it should not try to catch up.
                    this.GameState.ResetElapsedTime();

                    this.GotoNextScreen();
                }
            }
            finally
            {
                this.updating = false;
            }
        }

        #endregion

        #region Background Thread

        /// <summary>
        /// Worker thread draws the loading animation and updates the network
        /// session while the load is taking place.
        /// </summary>
        void BackgroundWorkerThread()
        {
            long lastTime = Stopwatch.GetTimestamp();

            // EventWaitHandle.WaitOne will return true if the exit signal has
            // been triggered, or false if the timeout has expired. We use the
            // timeout to update at regular intervals, then break out of the
            // loop when we are signalled to exit.
            while (!backgroundThreadExit.WaitOne(1000 / 30))
            {
                GameTime gameTime = this.GetGameTime(ref lastTime);

                this.DrawLoadAnimation(gameTime);
            }
        }

        /// <summary>
        /// Works out how long it has been since the last background thread update.
        /// </summary>
        GameTime GetGameTime(ref long lastTime)
        {
            long currentTime = Stopwatch.GetTimestamp();
            long elapsedTicks = currentTime - lastTime;
            lastTime = currentTime;

            TimeSpan elapsedTime = TimeSpan.FromTicks(elapsedTicks * TimeSpan.TicksPerSecond / Stopwatch.Frequency);

            return new GameTime(loadStartTime.TotalGameTime + elapsedTime, elapsedTime);
        }

        /// <summary>
        /// Calls directly into our Draw method from the background worker thread,
        /// so as to update the load animation in parallel with the actual loading.
        /// </summary>
        void DrawLoadAnimation(GameTime gameTime)
        {
            if ((this.GraphicsDevice == null) || this.GraphicsDevice.IsDisposed)
                return;

            try
            {
                // Update child controls
                this.GameState.Tick();

                // Draw the loading screen.
                //this.Draw(gameTime);
                //this.GraphicsDevice.Present();
            }
            catch
            {
                // If anything went wrong (for instance the graphics device was lost
                // or reset) we don't have any good way to recover while running on a
                // background thread. Setting the device to null will stop us from
                // rendering, so the main game can deal with the problem later on.
                Debug.Assert(false, "There was an unhandled exception during DrawLoadAnimation.");
                this.GraphicsDevice.Dispose();
            }
        }

        #endregion
    }
}
